import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTool, relatedTools, allPublicSlugs } from "@/lib/public/queries"; import { renderMarkdown } from "@/lib/text"; import { label, fmtDate } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { LogoMark } from "@/components/ui/logo-mark"; import { Breadcrumbs } from "@/components/breadcrumbs"; import { ToolCard } from "@/components/tool-card"; import { ReportForm } from "@/components/report-form"; import { ExternalLink, BookOpen, GitBranch, Receipt, Scale, ShieldCheck, Lock, AlertTriangle, Check, Minus, Scale3D } from "lucide-react"; import { APP_URL } from "@/lib/env"; export const revalidate = 600; export async function generateStaticParams() { try { return (await allPublicSlugs()).slice(0, 500).map((r) => ({ slug: r.slug })); } catch { return []; } } export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { const t = await getTool((await params).slug); if (!t) return { title: "Not found" }; return { title: `${t.name} — ${t.summary.slice(0, 60)}`, description: t.summary, alternates: { canonical: `/tools/${t.slug}` }, openGraph: { title: t.name, description: t.summary, type: "article", url: `${APP_URL}/tools/${t.slug}` } }; } function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) { return

{title}

{children}
; } function List({ items, icon }: { items: string[]; icon?: "check" | "minus" }) { if (!items.length) return

Not documented.

; const I = icon === "minus" ? Minus : Check; return ; } function Row({ k, v }: { k: string; v: React.ReactNode }) { return
{k}
{v}
; } const chips = (xs: string[]) => (xs.length ? xs.map((x) => {label(x)}) : —); export default async function ToolPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const t = await getTool(slug); if (!t) notFound(); const related = await relatedTools(t.id); const primary = t.categories.find((c) => c.isPrimary) ?? t.categories[0]; const html = renderMarkdown(t.description); const links = [ { href: t.homepageUrl, label: "Official site", icon: ExternalLink, field: "homepage_url" }, { href: t.docsUrl, label: "Documentation", icon: BookOpen, field: "docs_url" }, { href: t.repoUrl, label: "Repository", icon: GitBranch, field: "repo_url" }, { href: t.pricingUrl, label: "Pricing", icon: Receipt, field: "pricing_url" }, { href: t.licenseUrl, label: "License", icon: Scale, field: "license_url" }, { href: t.privacyUrl, label: "Privacy policy", icon: Lock, field: "privacy_url" }, { href: t.securityUrl, label: "Security", icon: ShieldCheck, field: "security_url" }, ].filter((l) => l.href); const lc = new Map(t.linkChecks.map((l) => [l.field, l])); const jsonLd = { "@context": "https://schema.org", "@type": "SoftwareApplication", name: t.name, url: t.homepageUrl, description: t.summary, ...(t.operatingSystems.length ? { operatingSystem: t.operatingSystems.map(label).join(", ") } : {}), ...(primary ? { applicationCategory: primary.name } : {}), ...(t.vendor ? { author: { "@type": "Organization", name: t.vendor.name, ...(t.vendor.websiteUrl ? { url: t.vendor.websiteUrl } : {}) } } : {}), ...(t.pricingModel.includes("free") || t.pricingModel.includes("open-source") ? { offers: { "@type": "Offer", price: "0", priceCurrency: "USD" } } : {}), ...(t.licenseSpdx && t.licenseUrl ? { license: t.licenseUrl } : {}), }; const breadcrumbLd = { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [{ "@type": "ListItem", position: 1, name: "Home", item: APP_URL }, ...(primary?.parent ? [{ "@type": "ListItem", position: 2, name: primary.parent.name, item: `${APP_URL}/categories/${primary.parent.slug}` }] : []), ...(primary ? [{ "@type": "ListItem", position: 3, name: primary.name, item: `${APP_URL}/categories/${primary.slug}` }] : []), { "@type": "ListItem", position: 4, name: t.name, item: `${APP_URL}/tools/${t.slug}` }] }; return (